﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Lidgren.Network;
using System.Reflection;
using System.Net;
using System.Threading;
using Kaleta.Network.Serialization;
using System.Diagnostics;
using Kaleta.Network.Serialization.Types;

namespace Kaleta.Network
{
    public enum DistributionType : byte
    {
        CreateNew,
        Update,
        Dispose,
        GetAllDistributedObjects,
    }

    /// <summary>
    /// The access point for distribution of objects. Use it to create and dispose of distributed objects.
    /// On client side use the DistributableObjects dictionary to get to all synchronized objects.
    /// </summary>
    public class DistributionSession
    {

        //---------------------------------------------------------------------------------------
        //Server:

        /// <summary>
        /// Should be used only on server side.
        /// Reads all client's messages and updates server's objects.
        /// Messages from clients are distributed to all clients.
        /// </summary>
        /// <param name="obj"></param>
        public static void ServerUpdate()
        {
            if (Server == null)
                throw new DistributableObjectException("DistributionSession.Server is null. Attach it to an existing Server first.");


            NetMessageType messageType;
            NetConnection sender;

            serverReadBuffer.Reset();

            
            while (Server.ReadMessage(serverReadBuffer, out messageType, out sender))
            {
                if (messageType == NetMessageType.Data)
                {
                    NetBuffer[] resendBuffer = new NetBuffer[MAX_CHANNELS];

                    for (int n = 0; n < MAX_CHANNELS; n++)
                        resendBuffer[n] = Server.CreateBuffer();

                    while (serverReadBuffer.Position < serverReadBuffer.LengthBits)
                        DistributionSerializer.Deserialize(serverReadBuffer, ServerDistributableObjects,
                            resendBuffer, sender);

                    //distribute new messages to clients depending on channels:
                    for (int n = 0; n < MAX_CHANNELS; n++)
                        if (resendBuffer[n].LengthBits > 0)
                            Server.SendToAll(resendBuffer[n], (NetChannel)n);

                }

                else
                {
                    if (ClientDataRecieved != null)
                        ClientDataRecieved(null, new NetworkDataRecievedEventArgs(messageType, serverReadBuffer, sender));

                }
            }
        }

        //---------------------------------------------------------------------------------------
        //Client:

        /// <summary>
        /// Should be used only on Client side. 
        /// Requests the server for creation of this object.
        /// </summary>
        /// <typeparam name="T">The type of object to create</typeparam>
        /// <returns>New object which is not yet created on server.</returns>
        public static T CreateObject<T>() where T : DistributableObject, new()
        {
            #region Assertions
#if DEBUG
            if (Client == null)
                throw new DistributableObjectException("DistributionSession.Client is null. Attach it to an existing client first.");
            if (!DistributionSerializer.AllDistributedProperties.ContainsKey( typeof(T)))
                throw new DistributableObjectException("The type '" + typeof(T).FullName + "' is not registred in the distribution type dictionary OR the distributed property has not the 'Distribute' attribute.");

#endif
            #endregion

            T obj = new T();
            obj.Key = keyGenerator.Next();
            obj.ClientOwnerID = ClientID;
            obj.IsDistributed = false;

            DistributableObjects.Add(obj.Key, obj);

            //here notify server about creation with the hash of object and its type

            NetBuffer sendBuffer = Client.CreateBuffer();

            DistributionSerializer.Serialize(sendBuffer, DistributionType.CreateNew, obj, DistributionSerializer.AllDistributedProperties[typeof(T)][0], null, NetChannel.ReliableInOrder1);
            Client.SendMessage(sendBuffer, NetChannel.ReliableInOrder1);

            return obj;
        }

        

        /// <summary>
        /// Should be used only on Client side. 
        /// Requests the server for creation of this object.
        /// 
        /// </summary>
        /// <param name="obj"></param>
        public static void DistributeObject(DistributableObject obj)
        {
            #region Assertions
#if DEBUG
            if (Client == null)
                throw new DistributableObjectException("DistributionSession.Client is null. Attach it to an existing client first.");
            if (!DistributionSerializer.AllDistributedProperties.ContainsKey(obj.GetType()))
                throw new DistributableObjectException("The type '" + obj.GetType().FullName + "' is not registred in the distribution type dictionary OR the distributed property has not the 'Distribute' attribute.");

#endif
            #endregion

            obj.Key = keyGenerator.Next();
            obj.ClientOwnerID = ClientID;
            obj.IsDistributed = false;

            DistributableObjects.Add(obj.Key, obj);

            NetBuffer sendBuffer = Client.CreateBuffer();

            //create new on server:

            DistributionSerializer.Serialize(sendBuffer, DistributionType.CreateNew, obj, DistributionSerializer.AllDistributedProperties[obj.GetType()][0], null, NetChannel.ReliableInOrder1);

            foreach(var prop in DistributionSerializer.AllDistributedProperties[obj.GetType()])
               DistributionSerializer.Serialize(sendBuffer, DistributionType.Update, obj, prop, prop.GetValue(obj, null), NetChannel.ReliableInOrder1);

            Client.SendMessage(sendBuffer, NetChannel.ReliableInOrder1);
        }




        /// <summary>
        /// Should be used only on client side.
        /// This will 
        /// - poll (read) all server's messages 
        /// - update client's objects 
        /// - send all pending 'update' messages to server
        /// </summary>
        public static void ClientUpdate()
        {
            //Serialized object has this info:
            //a)Type of operation on object: CreateNew, Update, Disposal
            //b)Object's Key
            //d)Properties to update : present if Type=Update
            //e)New values on these properties : present if Type=Update

            if (Client == null)
                throw new DistributableObjectException("DistributionSession.Client is null. Attach it to an existing client first.");

            NetMessageType messageType;

            //Read from server:
            clientReadBuffer.Reset();

            while (Client.ReadMessage(clientReadBuffer, out messageType))
            {
                if (messageType == NetMessageType.Data)
                    while (clientReadBuffer.Position < clientReadBuffer.LengthBits)
                        DistributionSerializer.Deserialize(clientReadBuffer, DistributableObjects, null, null);

                if (messageType == NetMessageType.StatusChanged && Client.Status == NetConnectionStatus.Connected)
                {
                    //send a request to get all distributed objects from server:
                    clientSendBuffer[(int)NetChannel.ReliableInOrder1].Write((byte) NetChannel.ReliableInOrder1);
                    clientSendBuffer[(int)NetChannel.ReliableInOrder1].Write((byte) DistributionType.GetAllDistributedObjects);
                }

                else
                    if (ServerDataRecieved != null)
                        ServerDataRecieved(null, new NetworkDataRecievedEventArgs(messageType, clientReadBuffer, null));
            }

            //Send to server:

            for (int channel = 0; channel < MAX_CHANNELS; channel++)
                if (clientSendBuffer[channel].LengthBits > 0)
                {
                    Client.SendMessage(clientSendBuffer[channel], (NetChannel)channel);
                    clientSendBuffer[channel] = Client.CreateBuffer();
                }

        }

        /// <summary>
        /// Should be used only on client side.
        /// This will remove this object from server and then from all clients.
        /// </summary>
        /// <param name="obj">Object to dispose of</param>
        static public void Dispose(DistributableObject obj)
        {
            NetBuffer sendBuffer = Client.CreateBuffer();
            DistributionSerializer.Serialize(sendBuffer, DistributionType.Dispose, obj, DistributionSerializer.AllDistributedProperties[obj.GetType()][0], null, NetChannel.ReliableInOrder1);

            DistributableObjects.Remove(obj.Key);

            Client.SendMessage(sendBuffer, NetChannel.ReliableInOrder1);
        }


        /// <summary>
        /// Attaches an existing server and creates network buffer for sending.
        /// Registers also types for distribution. Specified types must derive from type DistributableObject.
        /// </summary>
        /// <param name="server">Server to attach</param>
        /// <param name="typesToRegister">Types to register for distribution.</param>
        static public void Initialize(NetServer server, Type[] typesToRegister)
        {
            Server = server;

            serverReadBuffer = Server.CreateBuffer();

            foreach (Type type in typesToRegister)
                RegisterClass(type);

            LatencyTester = new LatencyTester();
            RegisterClass(typeof(NetLatencyObject));
            DistributionSerializer.CustomTypesForSerialization[typeof(DistributableString)] = new DistributableStringSerializer();
        }

        /// <summary>
        /// Attaches an existing client and creates network buffer for sending.
        /// Registers also types for distribution. Specified types must derive from type DistributableObject.
        /// </summary>
        /// <param name="client">Client to attach</param>
        /// <param name="typesToRegister">Types to register for distribution.</param>
        static public void Initialize(NetClient client, Type[] typesToRegister)
        {
            Client = client;

            CreateClientBuffers();

            foreach (Type type in typesToRegister)
                RegisterClass(type);

            LatencyTester = new LatencyTester();
            RegisterClass(typeof(NetLatencyObject));
            DistributionSerializer.CustomTypesForSerialization[typeof(DistributableString)] = new DistributableStringSerializer();

            ClientID = keyGenerator.Next();
        }

        /// <summary>
        /// Attaches an existing server and client and creates network buffer for sending.
        /// Registers also types for distribution. Specified types must derive from type DistributableObject.
        /// </summary>
        /// <param name="server">Server to attach</param>
        /// <param name="client">Client to attach</param>
        /// <param name="typesToRegister">Types to register for distribution.</param>
        static public void Initialize(NetServer server, NetClient client, Type[] typesToRegister)
        {
            Server = server;
            Client = client;

            CreateClientBuffers();
            serverReadBuffer = Server.CreateBuffer();

            foreach (Type type in typesToRegister)
                RegisterClass(type);

            LatencyTester = new LatencyTester();
            RegisterClass(typeof(NetLatencyObject));
            DistributionSerializer.CustomTypesForSerialization[typeof(DistributableString)] = new DistributableStringSerializer();

            ClientID = keyGenerator.Next();
        }

        static public void RegisterClass(Type type)
        {
            DistributionSerializer.RegisterClass(type);
        }

        static private void CreateClientBuffers()
        {
            for (int channel = 0; channel < MAX_CHANNELS; channel++)
                clientSendBuffer[channel] = Client.CreateBuffer();

            clientReadBuffer = Client.CreateBuffer();
        }

        /// <summary>
        /// Should be used on client side.
        /// Fires if data from Server recieved except messages of NetMessageType.Data.
        /// </summary>
        public static event EventHandler<NetworkDataRecievedEventArgs> ServerDataRecieved;

        /// <summary>
        /// Should be used on server side.
        /// Fires if data from Client recieved except messages of NetMessageType.Data.
        /// </summary>
        public static event EventHandler<NetworkDataRecievedEventArgs> ClientDataRecieved;

        /// <summary>
        /// Should be used only on client side.
        /// This is the client's collection of all network-distributed objects.
        /// These objects are synchronized (e.g. share the same values of distributed properties) in server and all clients.
        /// Changes to an distributed property of an object in this collection will distribute over the network.
        /// Use these objects in your Draw and Update methods.
        /// </summary>
        public static Dictionary<int, DistributableObject> DistributableObjects = new Dictionary<int, DistributableObject>();

        public static NetClient Client { get; set; }

        public static int ClientID { get; private set; }

        /// <summary>
        /// Specifies the server of distribution.
        /// If the server is not null, then this application hosts the distribution process, e.g. this app is Host.
        /// </summary>
        public static NetServer Server { get; set; }
        public static LatencyTester LatencyTester { get; protected set; }

        //todo: change to private:
        internal static NetBuffer clientReadBuffer;
        
        private static int MAX_CHANNELS = 32;

        internal static NetBuffer[] clientSendBuffer = new NetBuffer[MAX_CHANNELS];

        internal static NetBuffer serverReadBuffer;

        

        /// <summary>
        /// Internal collection of server's distributable objects.
        /// </summary>
        internal static Dictionary<int, DistributableObject> ServerDistributableObjects = new Dictionary<int, DistributableObject>();
        private static UniqueKeyGenerator keyGenerator = new UniqueKeyGenerator(Guid.NewGuid().GetHashCode());
    }

    public class NetworkDataRecievedEventArgs : EventArgs
    {
        public NetMessageType NetMessageType { get; private set; }
        public NetBuffer ReadBuffer { get; private set; }
        public NetConnection Sender { get; private set; }

        public NetworkDataRecievedEventArgs(NetMessageType type, NetBuffer buffer, NetConnection sender)
        {
            this.NetMessageType = type;
            this.ReadBuffer = buffer;
            this.Sender = sender;
        }
    }
}
